ユーザーをログアウトから守れ!―シーケンス図から読み解くログイン状態維持【Webアプリ編】
生魚おじさん、都元です。今月の魚はアジです!アジを食べましょう。
さて、認証というのは面倒なもので、利用者に余計な手間を掛けさせてアクティブ率を下げたくないと日夜工夫を凝らす我々にとっては、やり玉に上がりやすいテーマであると思います。要するに、ユーザーをログアウトさせたくないわけです。
例えば Facebook や Twitter のページはいつ訪問しても自分のアカウントでログイン状態になっています。 最後にログインしたのはいつでしたっけ? 覚えていませんよね? これがおそらく皆さんの理想です。
セッションによるログイン
ログインには通常、Cookie を使ったセッションという仕組みを利用しています。が、このセッションはアプリケーションにもよりますが、多くの場合最終アクセスから 30 分ほどで期限が切れてしまいます。30 分の間隔を開けずに滞在状態を続ければ、それはログイン状態維持が実現できますが、そういうわけにもいかないでしょう。
結論としてセッションだけでログイン状態維持は無理なんですが、全ての基本として、まずはシーケンス図を追って行きましょう。
- まず、有効なセッションが無い状態でWebアプリにランディングした状態を考えます。
- リクエストを受けたWebアプリは、当然このユーザーが誰なのか、判断できません。
- そのため、ユーザーにはログインフォームを提示します。
- ユーザーは ID とパスワードを入力して送信します。
- Webアプリはパスワードを検証します。
- 問題なければ、セッションを確立し、ログインが成立します。
- Webアプリは、リクエストされたコンテンツを返しつつ、Cookieで「セッションID」を返します。
- 後続のリクエストについては、全て「セッションID」をCookieで送り続けます。
- Webアプリはユーザーが送ってきたセッションIDを確認し、ユーザーを特定します。
- そのユーザーに向けたコンテンツを返します。
- その後ユーザーはWebアプリから離脱し、セッションタイムアウトが発生します。このときWebアプリは自動的にセッションを破棄し、ログアウトします。
- ユーザーの再訪問があったとき、(例え古いセッションIDを送ってきたとしても)
- Webアプリ上には該当するセッションはありません。
- 従って、再認証をすべく、ユーザーにはログインフォームを提示せざるを得ません。
これが最もベーシックな動きです。言うなれば30分以内に再リクエストを続けている限り、永続的なログイン状態を維持できると言えますが、それは皆さんのやりたいことではないはずです。
自動ログインCookieによるログイン状態維持
では、Facebook や Twitter では何をしているんでしょうか? まぁ、これらのサービスを調べたわけではないので実際に何をしているのかは正直わかりません。私自身は Spring Security という Java のフレームワークをよく利用するため、そのしくみを以下に解説します。
- 同様に、有効なセッションが無い状態でWebアプリにランディングした状態から始めましょう。
- リクエストを受けたWebアプリは、当然このユーザーが誰なのか、判断できません。
- そのため、ユーザーにはログインフォームを提示します。ここまでは全く同じです。
- ユーザーは ID とパスワードを入力し、さらに「自動ログイン」のチェックボックスをONにして送信します。
- Webアプリはパスワードを検証します。
- 問題なければ、セッションを確立し、ログインが成立します。
- さらに「自動ログイン」を要求されているため、セッションとは別の仕組みで自動ログインCookieの値 (Remember-meトークン) を生成し、ユーザー名などと一緒に永続化しておきます。(※)
- Webアプリは、リクエストされたコンテンツを返しつつ、Cookieで「セッションID」と「Remember-meトークン」を返します。
- 後続のリクエストについては、全て「セッションID」をCookieで送り続けます。「Remember-meトークン」も送信することになりますが、この時点ではあまり意味はありません。
- Webアプリはユーザーが送ってきたセッションIDを確認し、ユーザーを特定します。
- そのユーザーに向けたコンテンツを返します。
- その後ユーザーはWebアプリから離脱し、セッションタイムアウトが発生します。このときWebアプリは自動的にセッションを破棄し、ログアウトします。ただし、Remember-meトークンを破棄したりはしません。
- ユーザーの再訪問があったとき、「Remember-meトークン」と共にリクエストが発生します。
- 当然セッションは切れていますが、
- Remember-meトークンは検証できます。
- このトークンが正しければ、ここで新たなセッションを確立し、自動ログインが成立します。
- Webアプリは、リクエストされたコンテンツを返しつつ、Cookieで「セッションID」と新たな「Remember-meトークン」を返します。
※ この説明は、シンプル化のために色々な要素を省略しています。Remember-me の仕組みは、利便性とのトレードオフで若干セキュリティが低下します。Spring Security では実際には様々な工夫を凝らしてセキュリティの低下を軽減しています。独自でこのような仕組みを実装する際は、じっくりと調査検討を行いましょう。
たとえセッションの有効期限が 30 分だったとしても、Remember-me トークンの有効期限が 30 日であれば、30日以内に再訪問を続けている限り、ログイン状態を維持できます。
セッション有効期間とRemember-meトークン有効期間の決め方
さて、ではセッション有効期限と、Remember-meトークン有効期間というのはどのように決めたらいいのでしょうか? ちなみに、これらの有効期限は「最終リクエストから」一定期間で切れる、という特徴を持っていることに注意しましょう。
これらの有効期限を決めるために、まずはWebアプリに対するユーザーの挙動として、次のような統計情報を用意します。
- 滞在時間 (ランディングから離脱までの時間)
- 滞在中のリクエスト間隔 (前回のレスポンスを返してから、次のリクエストを送るまでの時間)
- 再訪問間隔 (前回の離脱から、次のランディングまでの時間)
セッションの有効期間は、「滞在中のリクエスト間隔」の 90 パーセンタイル辺り... と言いたいところですが、まぁそんな難しく考えず、30 分決め打ちでいいような気もします。切れちゃっても、Remember-me で自動ログインできるわけですから。
次にRemember-meトークン有効期間ですが、これは例えば「再訪問間隔」の 90〜99.9 パーセンタイル辺りを狙っていきましょう。つまり 0.1〜10 パーセントのユーザーが、再訪問時に再認証を求められる、というような計算です。
全員を救うことはできません。であれば、何パーセント救えばいいですか? という議論をするのです。
あ、滞在時間は関係ありませんでしたね :P
OAuth 2.0 によるログイン状態維持
昨今は OAuth 認証が一般的になっています。前述の通り、(仕組みは調べていませんが) Facebook や Twitter には自動ログイン相当の機能が備わっています。ので、わざわざ自分でその仕組を作るのではなく、IdP (Facebook や Twitter) の持っているしくみに乗っかってしまえばどうでしょう? というのが本セクションのテーマです。
ちょっと長くなりますが、シーケンスを見て行きましょう。
- 同様に、有効なセッションが無い状態でWebアプリにランディングした状態から始めましょう。
- リクエストを受けたWebアプリは、当然このユーザーが誰なのか、判断できません。ここまでは全く同じです。
- そのため、IdP に向けてリダイレクトを行います。ここから先、17番までは普通の OAuth Dance ですので、ご存知の方は読み飛ばしてかまいません。
- IdP に対して OAuth 2.0 の Authorization Request を実行します。
- この時、IdP ではまだログインしていないと仮定すると、ここで IdP のログインフォームが返ります。
- ユーザーは IdP の ID / pass を送信します。
- IdP ではこの内容を確認します。
- 問題なければ IdP 上でのセッションが確立します。
- Authorization Response では、Webアプリに対するリダイレクトと共に「セッションID」を発行します。
- 再びリダイレクトでWebアプリに戻ってきます。この時、URL のクエリには Authorization code が付いています。
- Authorization code を受け取ったWebアプリは、OAuth 2.0 の Token Request を実行し、
- アクセストークンを受け取ります。
- このアクセストークンを使って、Token Introspectionを行うなり、Profile API を叩くなり。
- これによってユーザー名を確認します。
- ここで確認できたユーザー名で認証が成立します。必要であればアクセストークンをどこかに保存しましょう。
- 無事、Webアプリ上でもセッションを確立し、ログインが成立します。
- Webアプリは、リクエストされたコンテンツを返しつつ、Cookieで「セッションID」を返します。このセッションID は、9番の IdP におけるセッションID とは異なることに注意が必要です。
- 後続のリクエストについては、全て「セッションID」をCookieで送り続けます。
- Webアプリはユーザーが送ってきたセッションIDを確認し、ユーザーを特定します。
- ■■ 上記19番が推奨の処理です。この20番および21番の処理は非推奨です。
- ■■ このフェーズで 13 番のような Token Introspection や Profile API を叩くことはしないでください。
- そのユーザーに向けたコンテンツを返します。
- その後ユーザーはWebアプリから離脱し、セッションタイムアウトが発生します。このときWebアプリは自動的にセッションを破棄し、Webアプリからログアウトします。
- ユーザーの再訪問があったとき、(例え古いセッションIDを送ってきたとしても)
- Webアプリ上には該当するセッションはありません。
- 従って、再認証をすべく、IdP に向けてリダイレクトを行います。
- IdP に対して OAuth 2.0 の Authorization Request を実行します。
- この時、IdP 上でのセッションは生きている、または自動ログインが有効だと仮定します。
- すると、再びリダイレクトでWebアプリに戻ってきます。この時、URL のクエリには Authorization code が付いています。
- 残りは10〜17番のステップと全く同じように新たなセッションを自動で確立できます。
OAuth 2.0 によるログイン状態維持 (IdP に自動ログインCookieを適用)
上記のシーケンスでは WebApp のセッション維持にフォーカスしていましたので、IdP 側のセッション維持には少ししか触れませんでした。 最後に、IdP 側には自動ログイン cookie をの仕組みを適用したシーケンスを示します。
まとめ
と、いうわけで。ユーザーをログアウトさせたくない! という要件を満たすために、独自の自動ログインを実装する方法と、ソーシャルログインによる実装方法を見てきました。
少し OAuth を知っている方は「リフレッシュトークンを使うのかな?」と予想したかもしれません。 が、実はこのしくみにリフレッシュトークンは必須じゃないんですよね。自分も書いてみて気づきました。すごい (小並感)
また、本稿でお伝えしたいことは、具体的なシーケンスもさることながら、セッション等の有効期間の決め方でありました。
繰り返しになりますが、全員を「ログアウト」から救うことはできません。なので、何パーセントを救えばいいのか、という視点で考えることが重要です。
次回予告
今回は【Webアプリ編】でした。次回は【Mobileアプリ編】をお送りしたいと思います。
同様にシーケンスを追っていき、アクセストークンおよびリフレッシュトークンの有効期間の決め方についても触れていきたいと思っています。
お楽しみに。